/*
 * Copyright (c) 2018.
 * This file was written by Fancyears·Milos·Malvis @connor. Any question/bug you can't post to 1243093366@qq.com.
 * ALL RIGHTS RESERVED.
 */
package org.fancyears.sql;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.session.SqlSession;
import org.fancyears.annotation.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 
 * 
 * <p>
 * 通用sql实现类，主要是提前获取一部分namspace和检查session状态， 并更加方便的直接调用mybatis的方法
 * </p>
 *
 * <p>
 * 这个类目前多线程不安全，有可能造成内存泄漏，原因是{@link ThreadLocal}中的ThreadLocalMap value 可能无法释放
 * 在java 1.8中已经做了优化，java 1.7未知
 * </p>
 * 
 * <p>
 * 在高并发访问的情况下，每一个线程的session都将不一样，为确保数据的完整性，<span style=
 * 'color:red'>建议手动commit，遇到异常的情况下进行手动rollback</span>
 * </p>
 * 
 * @author: Fancyears·Milos·Malvis
 * @CreateDate: 2018年12月7日 上午10:20:11
 * @Description:
 * @param <T> 任意的pojo对象
 */
public class BaseSQLUtil<T> implements IBaseSQLUtil<T> {
	/**
	 * 日志打印
	 */
	private static final Logger logger = LoggerFactory.getLogger(BaseSQLUtil.class);
	/**
	 * 调试模式，由外面的初始化控制
	 */
	public static boolean SQL_DEBUG = true;
	/**
	 * 线程本地变量。保存session，避免多线程直接对单例模式的session的直接操作造成异常
	 */
	private static final ThreadLocal<SqlSession> threadSession = new ThreadLocal<>();
	/**
	 * 自动拼接的前半部分
	 */
	private String prePart = null;
	/**
	 * 方法map
	 */
	// TODO 能否优化methodMap是唯一的？将所有的method都保存，不用反复去检验
	private static final Map<String, Set<String>> methodMap = new HashMap<>();;
	private Set<String> localMethodName;

	/**
	 * 无参的构造方法是为了方便获取namespace
	 */
	public BaseSQLUtil() {
		SqlSession sqlSession = threadSession.get();
		try {
			if (sqlSession == null) {
				sqlSession = FactoryManager.getSession();
				threadSession.set(sqlSession);
			}
		} catch (RuntimeException ex) {
			throw ex;
		}
		localMethodName = new HashSet<>();
		Component r = this.getClass().getAnnotation(Component.class);
		Method[] ms = null;
		Class<?> target = null;
		/*
		 * 如果接口和实现类已经互相对应上了，则取接口上的所有方法
		 */
		if (r != null) {
			target = r.target();
			prePart = r.target().getCanonicalName();
		} else {
			Type[] tx = this.getClass().getGenericInterfaces();
			if (null == tx || tx.length <= 0) {
				throw new RuntimeException(this.getClass().getCanonicalName() + "请务必实现一个dao层接口，如果不想实现dao层接口，请使用"
						+ Component.class + "注解标识dao层");
			}
			prePart = tx[0].getTypeName();
			target = this.getClass();
		}
		// 方法缓存记录
		if (!methodMap.containsKey(prePart)) {
			ms = target.getMethods();
			if (ms != null && ms.length > 0) {
				for (int i = 0; i < ms.length; i++) {
					Method method = ms[i];
					/* native方法不归属管理范畴 */
					if (Modifier.isNative(method.getModifiers())) {
						continue;
					}
					localMethodName.add(method.getName());
				}
				methodMap.put(prePart, localMethodName);
			}
		}
		if (SQL_DEBUG) {
			logger.info("自动加入interface前半部分namespace:" + prePart);
			System.out.println("自动加入interface前半部分namespace:" + prePart + "\n代理对象:" + prePart);
		}
		System.out.println();
	}

	/**
	 * 获取当前执行的方法名，产生一个Exception主要是方便获取到StackTrace中的数据
	 * 为什么取RuntimeException，我也不知道，可能是受spring+mybatis的事务影响吧
	 * 
	 * 获取当前操作的方法必须在一个新的方法里面查找调用栈信息，否则调用栈可能会出现异常数据
	 * 
	 * @return 当前执行dao的方法名字
	 */
	private String getCurrentExecutingMethodName() {
		return "." + getCurrentStackElement().getMethodName();
	}

	/**
	 * 获取当前执行的class name
	 * 
	 * @return
	 */
	@SuppressWarnings("unused")
	private String getCurrentExecutingClzName() {
		return getCurrentStackElement().getClassName();
	}

	/**
	 * 获取当前执行的方法名，产生一个Exception主要是方便获取到StackTrace中的数据
	 * 为什么取RuntimeException，我也不知道，可能是受spring+mybatis的事务影响吧
	 * 
	 * 获取当前操作的方法必须在一个新的方法里面查找调用栈信息，否则调用栈可能会出现异常数据
	 *
	 */
	private StackTraceElement getCurrentStackElement() {
		/** 为什么不用下面这一行主要是无法准确获取到执行的dao层的方法 **/
		// StackTraceElement[] elements = Thread.currentThread().getStackTrace();
		/** 虽然能准确获取到dao层的方法，但是估计会损耗很多的性能 */
		// TODO 性能优化方案: 1.直接传参，不再自动获取到dao层的方法进行映射
		StackTraceElement[] elements = new RuntimeException().getStackTrace();
		if (BaseSQLUtil.class.getName().equals(elements[2].getClassName())) {
			if (methodMap.get(prePart).contains(elements[3].getMethodName())) {
				return elements[3];
			} else {
				return elements[2];
			}
		} else {
			return elements[4];
		}
	}

	/**
	 * 获取到线程本地变量中的session，而不是全局的一个session
	 * 
	 * @return
	 */
	public SqlSession getSession() {
		makeSessionNotNull();
		return threadSession.get();
	}

	/**
	 * 保证线程本地变量不为空，防止使用线程启动的时候Dao对象不为null，实际上线程又不处于当前执行的线程中造成找不到session，因此需要重写获取一份session
	 */
	private void makeSessionNotNull() {
		SqlSession sqlSession = threadSession.get();
		if (sqlSession == null) {
			sqlSession = FactoryManager.getSession();
			threadSession.set(sqlSession);
		}
	}

	/**
	 * 当发生异常的情况下进行手动回滚
	 * 
	 * 考虑到多线程的情况下保证能rollbak，必须确保线程本地变量中的session不为null以免造成数据异常
	 */
	public void rollback() {
		makeSessionNotNull();
		if (SQL_DEBUG) {
			logger.warn("执行rollback");
			System.err.println("执行rollback");
		}
		SqlSession sqlSession = threadSession.get();
		try {
			if (sqlSession == null) {
				System.err.println("当前线程session为null，不执行rollback");
				return;
			}
		} catch (RuntimeException ex) {
			ex.printStackTrace();
		}
		sqlSession.rollback();
	}

	/**
	 * 手动提交
	 * 
	 * 考虑到多线程的情况下保证能commit，必须确保线程本地变量中的session不为null以免造成数据异常
	 */
	public void commit() {
		makeSessionNotNull();
		if (SQL_DEBUG) {
			logger.warn("执行rollback");
			System.err.println("执行commit");
		}
		SqlSession sqlSession = threadSession.get();
		try {
			if (sqlSession == null) {
				System.err.println("当前线程session为null，不执行commit");
				return;
			}
		} catch (RuntimeException ex) {
			ex.printStackTrace();
		}
		sqlSession.commit();
	}

	protected T selectOne(Object record) throws Exception {
		makeSessionNotNull();
		SqlSession sqlSession = threadSession.get();
		return sqlSession.selectOne(prePart + getCurrentExecutingMethodName(), record);
	}

	protected List<T> selectList(Object record) throws Exception {
		makeSessionNotNull();
		SqlSession sqlSession = threadSession.get();
		return sqlSession.selectList(prePart + getCurrentExecutingMethodName(), record);
	}

	protected int update(Object record) throws Exception {
		makeSessionNotNull();
		SqlSession sqlSession = threadSession.get();
		return sqlSession.update(prePart + getCurrentExecutingMethodName(), record);
	}

	protected int delete(Object record) throws Exception {
		makeSessionNotNull();
		SqlSession sqlSession = threadSession.get();
		return sqlSession.delete(prePart + getCurrentExecutingMethodName(), record);
	}

	/**
	 * 该方法比较特殊，即重写了接口的方法，又被子类实现，还能被其他类访问
	 */
	@Override
	public int insert(Object record) throws Exception {
		makeSessionNotNull();
		SqlSession sqlSession = threadSession.get();
		return sqlSession.insert(prePart + getCurrentExecutingMethodName(), record);
	}

	@Override
	public T selectByPrimaryKey(Object param) throws Exception {
		return selectOne(param);
	}

	@Override
	public int deleteByPrimaryKey(Object param) throws Exception {
		return delete(param);
	}

	@Override
	public int insertSelective(Object param) throws Exception {
		return insert(param);
	}

	@Override
	public int updateByPrimaryKeySelective(Object param) throws Exception {
		return update(param);
	}

	@Override
	public int updateByPrimaryKey(Object param) throws Exception {
		return update(param);
	}
}
